# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import base64
import hashlib
import hmac
import json
import logging
import requests
import time
import urllib.parse

from coin.base.atomic import AtomicLong
from coin.exchange.util.async_http_client import AsyncHttpClient
from coin.base.param_util import to_list

import coin.exchange.base.kr_rest.rest_client_base as rcb
from coin.exchange.base.kr_rest.auth_base import AuthKeyContainer
from coin.exchange.util.rate_limit import RateLimiter

# Kraken API:
# https://www.kraken.com/help/api
#
# Note that Kraken has a strict rate limit for private api usage.
# Here is the rule (from their website):
#
# API call rate limit
# We have safeguards in place to protect against abuse/DoS attacks as well as
# order book manipulation caused by the rapid placing and canceling of orders.
#
# Every user of our API has a "call counter" which starts at 0.
# Ledger/trade history calls increase the counter by 2.
# Place/cancel order calls do not affect the counter.
# All other API calls increase the counter by 1.
#
# The user's counter is reduced every couple of seconds, and if the counter
# exceeds the user's maximum API access is suspended for 15 minutes.
# Tier 2 users have a maximum of 15 and their count gets reduced
# by 1 every 3 seconds. Tier 3 and 4 users have a maximum of 20;
# the count is reduced by 1 every 2 seconds for tier 3 users,
# and is reduced by 1 every 1 second for tier 4 users.
#
# Although placing and cancelling orders does not increase the counter,
# there are separate limits in place to prevent order book manipulation.
# Only placing orders you intend to fill and keeping the rate down to 1 per
# second is generally enough to not hit this limit.


class KrakenAuthBase(object):
  def __init__(self, key_file, timeout=10, use_extra_keys=False):
    if use_extra_keys:
      self._auth_key_container = AuthKeyContainer.from_primary_key(key_file)
    else:
      self._auth_key_container = AuthKeyContainer.from_single_key(key_file)
    self._private_api_rel_url = '/0/private/'
    self._api_url = "https://api.kraken.com" + self._private_api_rel_url
    self._timeout = timeout
    self._nonce = int(time.time() * 1000)
    self._logger = logging.getLogger(__name__)

  @property
  def num_of_keys(self):
    return len(self._auth_key_container)

  def get_sign(self, endpoint, data, secret_key):
    url = self._private_api_rel_url + endpoint
    postdata = urllib.parse.urlencode(data)
    encoded = (data['nonce'] + postdata).encode()
    message = url.encode() + hashlib.sha256(encoded).digest()
    signature = hmac.new(base64.b64decode(secret_key), message, hashlib.sha512)
    signature = base64.b64encode(signature.digest()).decode()
    return signature


class KrakenSyncAuth(KrakenAuthBase):
  def __init__(self, key_file, timeout=10, use_extra_keys=False):
    super().__init__(key_file, timeout=timeout, use_extra_keys=use_extra_keys)
    self._rate_limit = RateLimiter(1, 1.0)

  def auth_request(self, endpoint, params=None):
    api_keys = self._auth_key_container.get_next_key_pair()
    header = {
        'API-Key': api_keys.access_key,
        'API-Sign': self.get_sign(endpoint, params, api_keys.secret_key),
    }

    full_url = self._api_url + endpoint
    try:
      response = requests.post(full_url, headers=header, data=params, timeout=self._timeout)
      return response.json()
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException')
      raise
    except json.JSONDecodeError:
      self._logger.exception('JsonDecodeException: %s', response.content)
      raise
    except Exception:
      self._logger.exception('Unknown exception: %s')
      raise

  def query_nocheck(self, endpoint, params=None):
    req_timestamp = time.time()
    response = self.auth_request(endpoint, params)
    res_timestamp = time.time()
    return rcb.RestUpdate(req_timestamp, res_timestamp, response)

  def query(self, endpoint, params=None, disable_rate_limit=False):
    if not disable_rate_limit:
      while not self._rate_limit.check():
        time.sleep(0.1)
    else:
      self._rate_limit.check()

    params = params or {}
    self._nonce += 1
    params['nonce'] = str(self._nonce)
    params = {key: value for key, value in params.items() if value is not None}
    return self.query_nocheck(endpoint, params=params)


class KrakenAsyncAuth(KrakenAuthBase):
  def __init__(self, key_file, timeout=10, max_connections=8, use_extra_keys=False):
    super().__init__(key_file, timeout, use_extra_keys)
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._nonce = AtomicLong(int(time.time() * 1000))

  def query(self, endpoint, params=None):
    api_keys = self._auth_key_container.get_next_key_pair()
    params = params or {}
    params['nonce'] = str(self._nonce.get_and_add(1))
    params = {key: value for key, value in params.items() if value is not None}
    header = {
        'API-Key': api_keys.access_key,
        'API-Sign': self.get_sign(endpoint, params, api_keys.secret_key),
    }

    full_url = self._api_url + endpoint
    future = self._http_client.post(full_url, headers=header, data=params, timeout=self._timeout)
    return future


class KrakenNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      timeout=10,
      max_connections=8,
      sync=True,
      use_extra_keys=False,
  ):
    if sync:
      self._auth_queryer = KrakenSyncAuth(key_file, timeout=timeout, use_extra_keys=use_extra_keys)
    else:
      self._auth_queryer = KrakenAsyncAuth(key_file,
                                           timeout=timeout,
                                           max_connections=max_connections,
                                           use_extra_keys=use_extra_keys)

    self._logger = logging.getLogger(__name__)
    self._logger.info('KrakenNativePrivateClient: num of keys = %d', self.num_of_keys)

  @property
  def num_of_keys(self):
    return self._auth_queryer.num_of_keys

  def _query(self, url, params=None):
    return self._auth_queryer.query(url, params=params)

  def query_account_balance(self):
    url = 'Balance'
    update = self._query(url)
    return update

  def query_trade_balance(self, aclass='currency', asset='ZUSD'):
    url = 'TradeBalance'
    params = {
        'aclass': aclass,
        'asset': asset,
    }
    update = self._query(url, params)
    return update

  def query_open_orders(self, trades=False, userref=None):
    url = 'OpenOrders'
    params = {
        'trades': trades,
        'userref': userref,
    }
    update = self._query(url, params)
    return update

  def query_closed_orders(self,
                          trades=False,
                          userref=None,
                          start=None,
                          end=None,
                          ofs=None,
                          closetime=None):
    assert closetime in ('open', 'close', 'both', None)
    url = 'ClosedOrders'
    params = {
        'trades': trades,
        'userref': userref,
        'start': start,
        'end': end,
        'ofs': ofs,
        'closetime': closetime,
    }
    update = self._query(url, params)
    return update

  def query_orders_info(self, txid, trades=False, userref=None):
    txid = to_list(txid)
    assert len(txid) <= 20
    url = 'QueryOrders'
    params = {
        'trades': trades,
        'userref': userref,
        'txid': ','.join(txid),
    }
    update = self._query(url, params)
    return update

  def query_trades_history(self, trade_type=None, trades=False, start=None, end=None, ofs=None):
    assert trade_type in ('all',
                          'any position',
                          'close position',
                          'closing position',
                          'no position',
                          None)
    url = 'TradesHistory'
    params = {
        'type': trade_type,
        'trades': trades,
        'start': start,
        'end': end,
        'ofs': ofs,
    }
    update = self._query(url, params)
    return update

  def query_trades_info(self, txid, trades=False):
    txid = to_list(txid)
    assert len(txid) <= 20, len(txid)
    url = 'QueryTrades'
    params = {
        'txid': ','.join(txid),
        'trades': trades,
    }
    update = self._query(url, params)
    return update

  def query_open_positions(self, txid, docalcs=False):
    txid = to_list(txid)
    url = 'OpenPositions'
    params = {
        'txid': ','.join(txid),
        'docalcs': docalcs,
    }
    update = self._query(url, params)
    return update

  def query_ledgers_info(
      self,
      aclass='currency',
      asset='ZUSD',
      ledger_type=None,
      start=None,
      end=None,
      ofs=None,
  ):
    asset = to_list(asset)
    assert ledger_type in ('all', 'deposit', 'withdrawal', 'trade', 'margin', None)
    url = 'Ledgers'
    params = {
        'aclass': aclass,
        'asset': ','.join(asset),
        'ledger_type': ledger_type,
        'start': start,
        'end': end,
        'ofs': ofs,
    }
    update = self._query(url, params)
    return update

  def query_ledgers(self, ledger_ids):
    ledger_ids = to_list(ledger_ids)
    url = 'QueryLedgers'
    params = {
        'id': ','.join(ledger_ids),
    }
    update = self._query(url, params)
    return update

  # Get maker/taker fee rate.
  def query_trade_volume(self, pair, fee_info=True):
    pair = to_list(pair)
    url = 'TradeVolume'
    params = {
        'pair': ','.join(pair),
        'fee_info': fee_info,
    }
    update = self._query(url, params)
    return update

  def query_add_standard_order(
      self,
      *,
      pair,
      side,
      ordertype,
      price,
      volume,  # order volume in lots
      price2=None,
      leverage='none',
      oflags=None,
      starttm=0,  # now
      expiretm=0,  # no expiration
      userref=None,  # client order id
      validate=None  # validate fields, all other values are treated as True.
  ):
    assert side in ('buy', 'sell')
    assert ordertype == 'limit'  # other types are not allowed.
    assert ordertype in ('market',
                         'limit',
                         'stop-loss',
                         'take-profit',
                         'stop-loss-profit',
                         'stop-loss-profit-limit',
                         'stop-loss-limit',
                         'take-profit-limit',
                         'trailing-stop',
                         'trailing-stop-limit',
                         'stop-loss-and-limit',
                         'settle-position')
    if oflags is not None:
      oflags = to_list(oflags)
      assert set(oflags) <= {'viqc', 'fcib', 'fciq', 'nompp', 'post'}, oflags

    url = 'AddOrder'
    params = {
        'pair': pair,
        'type': side,
        'ordertype': ordertype,
        'price': price,
        'price2': price2,
        'volume': volume,
        'leverage': leverage,
        'starttm': starttm,
        'expiretm': expiretm,
        'userref': userref,
        'validate': validate,
    }
    if oflags is not None:
      params['oflags'] = ','.join(oflags)

    update = self._query(url, params)
    return update

  def query_cancel_open_order(self, txid):
    url = 'CancelOrder'
    params = {
        'txid': txid,
    }
    update = self._query(url, params)
    return update
